﻿/////////////////////////////////////////////////////////////////
//
// this class contains utility functions to determine the 
// outline of an element using tesselation. This works well with
// Box2D Polygon shapes. 
//
// This code is a modified version of Pete Blois tesselator from
// http://blois.us/Expression.Samples/Expression.Samples.zip
//
/////////////////////////////////////////////////////////////////
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Collections.Generic;
using System.Windows.Media.Imaging;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Collision.Shapes;
using FarseerPhysics.Common;
using xna = Microsoft.Xna.Framework;

namespace Spritehand.FarseerHelper
{
    public class BoundaryHelperBox2d
    {
        Canvas _cnvGame;

        public static double pixelsPerMeter = 100;
        public static xna.Vector2 ScreenTopLeft { get; set; }
        public static xna.Vector2 ScreenBottomRight { get; set; }

        public static xna.Vector2 WorldTopLeft { get; set; }
        public static xna.Vector2 WorldBottomRight { get; set; }

        public BoundaryHelperBox2d(Canvas cnvGame)
        {
            _cnvGame = cnvGame;

            // TODO: tweak margin/etc of Canvas

            if (Double.IsNaN(_cnvGame.Width) || Double.IsNaN(_cnvGame.Height))
            {
                if (Double.IsNaN(_cnvGame.ActualWidth) || Double.IsNaN(_cnvGame.ActualHeight)
                    || _cnvGame.ActualWidth == 0 || _cnvGame.ActualHeight == 0)
                {
                    _cnvGame.Width = 800;
                    _cnvGame.Height = 480;
                }
                else
                {
                    _cnvGame.Width = _cnvGame.ActualWidth;
                    _cnvGame.Height = _cnvGame.ActualHeight;
                }
                System.Diagnostics.Debug.WriteLine("Your game canvas did not specify a width and height, so it was forced to 800x480.");
            }

            ScreenTopLeft = new xna.Vector2(0, 0);
            ScreenBottomRight = new xna.Vector2((float)_cnvGame.Width, (float)_cnvGame.Height);

            xna.Vector2 screenSize = new xna.Vector2((float)cnvGame.Width, (float)cnvGame.Height);
            float ratio = screenSize.X / screenSize.Y;
            float halfWorldSize = (float)GetWorldSize(screenSize) / 2.0f;
            xna.Vector2 extent = new xna.Vector2(-ratio * halfWorldSize, halfWorldSize);

            WorldTopLeft = extent;
            WorldBottomRight = -extent;
        }

        private double GetWorldSize(xna.Vector2 screenSize)
        {
            double worldSize = 25;
            worldSize = screenSize.X / pixelsPerMeter;
            return worldSize;
        }

        public List<Shape> CreateShapesFromElement(World simulator, FrameworkElement entity, bool forceRectangle)
        {
            List<Shape> shapeDefs = new List<Shape>();

            System.Windows.Shapes.Ellipse ellipse = entity as System.Windows.Shapes.Ellipse;
            if (forceRectangle)
            {
                PolygonShape polygonDef = new PolygonShape(0.1f);
                Rect rect = new Rect(0, 0, entity.ActualWidth, entity.ActualHeight);

                xna.Vector2 pos = new xna.Vector2(0F, 0F);
                // does the box have a rotation?
                double angle = 0;
                if (entity.RenderTransform is RotateTransform)
                {
                    angle = (entity.RenderTransform as RotateTransform).Angle;
                }
                else
                    if (entity.RenderTransform is CompositeTransform)
                    {
                        angle = (entity.RenderTransform as CompositeTransform).Rotation;
                    }

                float radAngle = (float)(Math.PI * angle / 180.0);

                xna.Vector2 worldSize = ScreenSizeToWorldSize(new Size(rect.Width / 2, rect.Height / 2));
                //polygonDef.SetAsBox(worldSize.X, worldSize.Y, new xna.Vector2(0,0), radAngle);
                polygonDef.SetAsBox(worldSize.X, worldSize.Y, new xna.Vector2(0, 0), 0);
                shapeDefs.Add(polygonDef);
            }
            else
                if (entity is System.Windows.Shapes.Rectangle)
                {
                    PolygonShape polygonDef = new PolygonShape(0.1f);
                    System.Windows.Shapes.Rectangle rectangle = entity as System.Windows.Shapes.Rectangle;
                    Rect rect = new Rect(0, 0, rectangle.ActualWidth, rectangle.ActualHeight);

                    xna.Vector2 pos = new xna.Vector2(0F, 0F);
                    // does the box have a rotation?
                    double angle = 0;
                    if (rectangle.RenderTransform is RotateTransform)
                    {
                        angle = (rectangle.RenderTransform as RotateTransform).Angle;
                    }
                    else
                        if (rectangle.RenderTransform is CompositeTransform)
                        {
                            angle = (rectangle.RenderTransform as CompositeTransform).Rotation;
                        }

                    float radAngle = (float)(Math.PI * angle / 180.0);

                    xna.Vector2 worldSize = ScreenSizeToWorldSize(new Size(rect.Width / 2, rect.Height / 2));
                    //polygonDef.SetAsBox(worldSize.X, worldSize.Y, new xna.Vector2(0,0), radAngle);
                    polygonDef.SetAsBox(worldSize.X, worldSize.Y, new xna.Vector2(0, 0), 0);
                    shapeDefs.Add(polygonDef);
                }
                else if (ellipse != null && ellipse.ActualHeight == ellipse.ActualWidth)
                {

                    xna.Vector2 worldRadius = ScreenSizeToWorldSize(new Size(ellipse.ActualWidth / 2, ellipse.ActualHeight / 2));

                    CircleShape circleDef = new CircleShape(worldRadius.Y, 0.1f);

                    shapeDefs.Add(circleDef);
                }
                else
                {
                    Point center;
                    IList<PointShape> shapes = Tesselate(entity, out center);
                    foreach (PointShape shape in shapes)
                    {
                        PolygonShape polygonDef = new PolygonShape(0.1f);

                        Vertices vertices = new Vertices();
                        for (int i = 0; i < shape.Points.Length; ++i)
                        {
                            shape.Points[i].X -= entity.ActualWidth / 2;
                            shape.Points[i].Y -= entity.ActualHeight / 2;

                            vertices.Add(ScreenSizeToWorldSize(shape.Points[i]));
                        }

                        if (IsValidShape(vertices))
                        {
                            // this is a valid (convex) shape
                            polygonDef.Set(vertices);
                            shapeDefs.Add(polygonDef);
                        }
                        else
                        {
                            // invalid shape, we'll fall back to a rectangle
                            polygonDef = new PolygonShape(0.1f);
                            Rect rect = new Rect(0, 0, entity.ActualWidth, entity.ActualHeight);
                            xna.Vector2 worldSize = ScreenSizeToWorldSize(new Size(rect.Width / 2, rect.Height / 2));
                            polygonDef.SetAsBox(worldSize.X, worldSize.Y);
                            shapeDefs.Clear();
                            shapeDefs.Add(polygonDef);
                            break;
                        }
                    }

                }

            return shapeDefs;
        }

        public bool IsValidShape(Vertices vertices)
        {
            bool isValidShape = true;
            // Ensure the polygon is convex and the interior
            // is to the left of each edge.
            for (int i = 0; i < vertices.Count; ++i)
            {
                int i1 = i;
                int i2 = i + 1 < vertices.Count ? i + 1 : 0;
                xna.Vector2 edge = vertices[i2] - vertices[i1];

                for (int j = 0; j < vertices.Count; ++j)
                {
                    // Don't check vertices on the current edge.
                    if (j == i1 || j == i2)
                    {
                        continue;
                    }

                    xna.Vector2 r = vertices[j] - vertices[i1];

                    // Your polygon is non-convex (it has an indentation) or
                    // has colinear edges.
                    float s;
                    MathUtils.Cross(ref edge, ref  r, out s);

                    if (s <= 0.0f)
                    {
                        isValidShape = false;
                        break;
                    }
                }
            }
            return isValidShape;
        }

        public xna.Vector2 ScreenToWorld(xna.Vector2 screen)
        {
            return ConvertCoordinates((float)screen.X, (float)screen.Y, ScreenTopLeft, ScreenBottomRight, WorldTopLeft, WorldBottomRight, true);
        }

        public xna.Vector2 WorldToScreen(xna.Vector2 world)
        {
            xna.Vector2 value = ConvertCoordinates(world.X, world.Y, WorldTopLeft, WorldBottomRight, ScreenTopLeft, ScreenBottomRight, true);

            return new xna.Vector2(value.X, value.Y);
        }

        public xna.Vector2 ScreenSizeToWorldSize(System.Windows.Size size)
        {
            return ConvertSize(new xna.Vector2((float)size.Width, (float)size.Height), ScreenTopLeft, ScreenBottomRight, WorldTopLeft, WorldBottomRight, true);
        }

        public xna.Vector2 ScreenSizeToWorldSize(System.Windows.Point size)
        {
            return ConvertSize(new xna.Vector2((float)size.X, (float)size.Y), ScreenTopLeft, ScreenBottomRight, WorldTopLeft, WorldBottomRight, true);
        }

        public xna.Vector2 WorldSizeToScreenSize(xna.Vector2 size)
        {
            xna.Vector2 result = ConvertSize(size, WorldTopLeft, WorldBottomRight, ScreenTopLeft, ScreenBottomRight, true);
            return result;
        }

        private float ScreenWidth
        {
            get { return (ScreenBottomRight - ScreenTopLeft).X; }
        }

        private float ScreenHeight
        {
            get { return (ScreenBottomRight - ScreenTopLeft).Y; }
        }

        private xna.Vector2 ConvertCoordinates(float x, float y, xna.Vector2 topLeft1, xna.Vector2 bottomRight1, xna.Vector2 topLeft2, xna.Vector2 bottomRight2, bool invertY)
        {
            float w1 = bottomRight1.X - topLeft1.X;
            float h1 = bottomRight1.Y - topLeft1.Y;
            float u = (x - topLeft1.X) / w1;
            float v = (y - topLeft1.Y) / h1;
            if (invertY)
            {
                v = 1 - v;
            }
            xna.Vector2 p = new xna.Vector2();
            p.X = ((1.0f - u) * topLeft2.X) + (u * bottomRight2.X);
            p.Y = ((1.0f - v) * topLeft2.Y) + (v * bottomRight2.Y);
            return p;
        }

        private xna.Vector2 ConvertSize(xna.Vector2 size, xna.Vector2 topLeft1, xna.Vector2 bottomRight1, xna.Vector2 topLeft2, xna.Vector2 bottomRight2, bool invertY)
        {
            float w1 = bottomRight1.X - topLeft1.X;
            float h1 = bottomRight1.Y - topLeft1.Y;
            float w2 = bottomRight2.X - topLeft2.X;
            float h2 = bottomRight2.Y - topLeft2.Y;
            float rw = (w2 / w1);
            float rh = (invertY ? -1 : 1) * (h2 / h1);
            xna.Vector2 p = new xna.Vector2();
            p.X = size.X * rw;
            p.Y = size.Y * rh;
            return p;
        }

        public IList<PointShape> Tesselate(FrameworkElement element, out Point center)
        {
            List<PointShape> shapes = new List<PointShape>();
            center = new Point();
#if SILVERLIGHT
            WriteableBitmap bitmap = new WriteableBitmap(element, null);
            bitmap.Render(element, null);

            Size bitmapSize = new Size(bitmap.PixelWidth, bitmap.PixelHeight);

            double angleIncrement = System.Math.PI / 100;
            double maxRadius = Math.Sqrt(bitmapSize.Height * bitmapSize.Height / 4 + bitmapSize.Width * bitmapSize.Width / 4) + 1;

            List<Ray> rays = new List<Ray>();
            //Point offset = new Point();

            System.Windows.Shapes.Path path = element as System.Windows.Shapes.Path;
            if (path != null
                && path.Stretch == Stretch.None
                && double.IsNaN(path.Width) && double.IsNaN(path.Height))
            {
                center = new Point(path.Data.Bounds.Left + path.Data.Bounds.Width / 2, path.Data.Bounds.Top + path.Data.Bounds.Height / 2);
                //offset = new Point(path.Data.Bounds.Left, path.Data.Bounds.Top);
            }
            else
                center = new Point(bitmapSize.Width / 2.0, bitmapSize.Height / 2.0);

            double angle = 0;
            while (angle <= 2 * Math.PI)
            {

                Ray currentRay = this.CastRay(angle, maxRadius, bitmap, bitmapSize, center);
                if (currentRay != null)
                {

                    if (this.CanCombine(currentRay, rays))
                    {
                        rays.Add(currentRay);
                    }
                    else
                    {
                        PointShape shape = this.Combine(rays);
                        if (shape != null)
                            shapes.Add(shape);

                        if (rays.Count > 0)
                        {
                            Ray lastRay = rays[rays.Count - 1];
                            rays.Clear();
                            rays.Add(lastRay);
                        }

                        rays.Add(currentRay);
                    }
                }
                else
                {
                    PointShape shape = this.Combine(rays);
                    if (shape != null)
                        shapes.Add(shape);

                    rays.Clear();
                }
                angle += angleIncrement;
            }

            PointShape lastShape = this.Combine(rays);
            if (lastShape != null)
                shapes.Add(lastShape);

            //foreach (PointShape shape in shapes) {
            //    for (int i = 0; i < shape.Points.Length; ++i) {
            //        shape.Points[i].X -= offset.X;
            //        shape.Points[i].Y -= offset.Y;
            //    }
            //}

#endif
            return shapes;
        }
#if SILVERLIGHT
        private bool CanCombine(Ray ray, IList<Ray> rays)
        {
            if (rays.Count <= 1)
                return true;

            Ray startRay = rays[0];
            Ray endRay = ray;


            for (int i = 1; i < rays.Count; ++i)
            {
                double distance = DistanceToLine(startRay.Inner, ray.Inner, rays[i].Inner);
                if (distance > 2)
                {
                    PointShape shape = this.Combine(rays);
                    if (shape == null)
                        return true;
                    return false;
                }

                distance = DistanceToLine(startRay.Outer, ray.Outer, rays[i].Outer);
                if (distance > 2)
                {
                    PointShape shape = this.Combine(rays);
                    if (shape == null)
                        return true;
                    return false;
                }
            }

            return true;
        }

        private PointShape Combine(IList<Ray> rays)
        {
            if (rays.Count < 2)
                return null;

            PointShape shape = new PointShape();
            Ray lastRay = rays[rays.Count - 1];
            shape.Points[0] = lastRay.Outer;
            shape.Points[1] = lastRay.Inner;
            shape.Points[2] = rays[0].Inner;
            shape.Points[3] = rays[0].Outer;

            if (this.Normalize(shape))
                return shape;

            return null;
        }

        private double DistanceToLine(Point lineStart, Point lineEnd, Point point)
        {
            double xDelta = lineEnd.X - lineStart.X;
            double yDelta = lineEnd.Y - lineStart.Y;

            double u = ((point.X - lineStart.X) * xDelta + (point.Y - lineStart.Y) * yDelta) / (xDelta * xDelta + yDelta * yDelta);

            Point closestPoint;
            if (u < 0)
                closestPoint = lineStart;
            else if (u > 1)
                closestPoint = lineEnd;
            else
                closestPoint = new Point(lineStart.X + u * xDelta, lineStart.Y + u * yDelta);

            return PhysicsControllerMain.DistanceBetweenTwoPoints(closestPoint, point);  // closestPoint.DistanceTo(point);
        }

        public static TimeSpan totalTime;
        private Ray CastRay(double angle, double maxRadius, WriteableBitmap bitmap, Size bitmapSize, Point center)
        {

            Ray ray = new Ray();

            int width = (int)bitmapSize.Width;
            int height = (int)bitmapSize.Height;

            bool fillFinal = false;
            bool foundAny = false;
            Point? previous = null;
            double cosAngle = System.Math.Cos(angle);
            double sinAngle = System.Math.Sin(angle);

            for (double radius = maxRadius; radius >= 0; --radius)
            {
                int x = (int)(radius * cosAngle + center.X);
                int y = (int)(radius * sinAngle + center.Y);

                if (x >= 0 && x < width &&
                    y >= 0 && y < height)
                {

                    if (!fillFinal)
                    {
                        if (bitmap.Pixels[y * width + x] != 0)
                        {
                            if (previous != null)
                                ray.Outer = previous.Value;
                            else
                                ray.Outer = new Point(x, y);
                            fillFinal = true;
                            foundAny = true;
                        }
                    }
                    else
                    {
                        if (bitmap.Pixels[y * width + x] == 0)
                        {
                            if (previous != null)
                                ray.Inner = previous.Value;
                            else
                                ray.Inner = new Point(x, y);
                            fillFinal = false;
                            foundAny = true;
                            break;
                        }
                    }

                    previous = new Point(x, y);

                }
            }
            if (fillFinal)
                ray.Inner = new Point(center.X, center.Y);
            /**/

            if (!foundAny)
                return null;

            return ray;
        }

        public bool Normalize(PointShape shape)
        {
            List<Point> points = new List<Point>(shape.Points);

            //if (DistanceToLine(points[1], points[3], points[2]) <= 1)
            //    return false;

            for (int i = 0; i < points.Count; ++i)
            {
                for (int j = i + 1; j < points.Count; ++j)
                    if (points[i] == points[j])
                        points.RemoveAt(j);
            }



            shape.Points = points.ToArray();

            if (shape.Points.Length < 3)
                return false;

            return true;
        }


        private class Ray
        {
            public Point Outer { get; set; }
            public Point Inner { get; set; }
        }
#endif
    }


    public class PointShape
    {
        public PointShape()
        {
            this.Points = new Point[4];
        }

        public PointShape(int count)
        {
            this.Points = new Point[count];
        }

        public Point[] Points { get; set; }


    }
}

